/*
* bootloader.c - openwii umod2, elegantly simple version
* Copyright 2006-2008 OpenWii (admin@openwii.org)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* Version 2.4 by Andii
*/

#include "../chiip/archdep.h"
#include <avr/boot.h>
#include "serial.h"
#include "drive.h"

#define BOOTLOADER __attribute__ ((section (".bootloader_func")))

BOOTLOADER static void blWritePage(uint16_t addr);
BOOTLOADER static void blWaitRWW(void);
BOOTLOADER static uint16_t blSwap(uint16_t data);
BOOTLOADER static uint16_t blSerialRequest(uint8_t command, uint32_t offset, uint16_t data);

__attribute__ ((section (".bootloader"))) void loader()
{
	uint16_t ret_word;
	uint16_t firm_len;
	uint16_t nb_recv;
	uint16_t crc16_calc;
	uint16_t crc16_lu;
	uint16_t addr, flash_addr;

	// disable interrupts.
	uint8_t sreg = SREG;
	cli();
	LEDOFF;

	// retrieve the length of the firmware (2 bytes len + firmware + 2 crc16 bytes)
	// len is only the length of the firmware itself
	firm_len = blSerialRequest(CMD_READ16, FLASH_UPDATE, 0);
	firm_len = blSwap(firm_len);

	if (firm_len <= MAXFIRMLEN)
	{
		nb_recv = 0;
		crc16_calc = 0;
		addr = MIRRORADDR;
		while (nb_recv < firm_len)
		{	// read the firmware word by word
			ret_word = blSerialRequest(CMD_READ16, FLASH_UPDATE + nb_recv + 2, 0);
			nb_recv += 2;

			// calculate the additive checksum
			crc16_calc = crc16_calc + (ret_word >> 8) + (ret_word & 0xff);

			// call the self programming routines
			boot_page_fill(addr,ret_word);
			boot_spm_busy_wait();

			if ((nb_recv % PAGESIZE) == 0)
			{	// call the write page routine
				blWritePage(addr);
				LEDTOG;
			}
			addr += 2;
		}

		if ((nb_recv % PAGESIZE) != 0)
		{	// call the write page routine for the last page
			blWritePage(addr);
		}

		// read the transmitted additive checksum
		crc16_lu = blSerialRequest(CMD_READ16, FLASH_UPDATE + nb_recv + 2, 0);
		crc16_lu = blSwap(crc16_lu);

		if (crc16_calc == crc16_lu)
		{	// if the checksum is OK, proceed to the real programming
			nb_recv = 0;
			flash_addr = APPLADDR;
			addr = MIRRORADDR;
			while (nb_recv < firm_len)
			{	// read the firmware word by word in the mirror adress space
				blWaitRWW();
				ret_word = pgm_read_word_near(addr);
				nb_recv += 2;

				// call the self programming routines
				boot_page_fill(flash_addr, ret_word);
				boot_spm_busy_wait();

				if ((nb_recv % PAGESIZE) == 0)
				{	// call the write page routine
					blWritePage(flash_addr);
					LEDTOG;
				}
				addr += 2;
				flash_addr +=2;
			}

			if ((nb_recv % PAGESIZE) != 0)
			{	// call the write page routine for the last page
				blWritePage(flash_addr);
			}
		}

		LEDOFF;
		blWaitRWW();
		blSerialRequest(CMD_WRITE16, CONFIG_XFER, 0);
	}

	SREG = sreg;	// restore interrupt if necessary

	// jump to application
	asm volatile
	(
		"clr r30\n\t"
		"clr r31\n\t"
		"ijmp\n\t"
	);
}

BOOTLOADER static void blWritePage(uint16_t addr)
{
	boot_page_erase(addr);
	boot_spm_busy_wait();
	boot_page_write(addr);
	boot_spm_busy_wait();
}

BOOTLOADER static void blWaitRWW(void)
{
	while (boot_rww_busy())
	{
		boot_rww_enable();
	}
}

BOOTLOADER static uint16_t blSwap(uint16_t data)
{
	return (data >> 8) | (data << 8);
}


// wait approx 1 us
BOOTLOADER static void blWaitus(uint16_t us)
{
	int16_t t0 = TCNT1;
	us *= (uint8_t)USTIMERDIV;	// us now = how long 1 ms takes to pass (this requires mathlibs and more space...)
	while ((TCNT1 - t0) < us);	// only works correctly if timer is not in compare mode -krall
}

// transmit 1 byte via serial interface
BOOTLOADER static void blTxByte(uint8_t byte)
{
	uint8_t y;
	for (y = 0; y < 8; y++)
	{
		CLKLOW;		// drive clock low
		blWaitus(SERCLKDLY);
		if (byte & 0x1)
		{
			OUTHIZ;	// make output pin highZ
		}
		else
		{
			OUTLOW;	// drive output pin low
		}
		byte >>= 1;
		CLKHIZ;		// HighZ clock pin
		blWaitus(SERCLKDLY);
	}
	blWaitus(100);
	CLKLOW;		// drive clock low
	OUTLOW;		// drive output low
}

// receive 1 byte via serial
BOOTLOADER static uint8_t blRxByte()
{
	uint8_t byte = 0;
	uint8_t y;
	for (y = 0; y < 8; y++)
	{
		CLKLOW;
		blWaitus(SERCLKDLY);
		byte |= GETIN << y;
		CLKHIZ;
		blWaitus(SERCLKDLY);
	}
	blWaitus(100);
	CLKLOW;
	return byte;
}

// activate a serial command
BOOTLOADER static uint16_t blSerialRequest(uint8_t command, uint32_t offset, uint16_t data)
{
	uint8_t buffer[SERIAL_CMD_SIZE];	// command Buffer
	uint16_t ret;
	uint8_t x;
	uint8_t size = command >> 4;

	command |= 0xf0;
	buffer[0] = command;
	buffer[1] = 0;
	buffer[2] = (uint8_t)(offset >> 8);
	buffer[3] = (uint8_t)offset;
	buffer[4] = (uint8_t)data;
	buffer[5] = (uint8_t)(offset >> 16);
	buffer[6] = (uint8_t)(data >> 8);
	buffer[7] = 0;
	buffer[8] = size;
	buffer[9] = 0;

	for (x = 0; x < SERIAL_CMD_SIZE; x++)
	{
		blTxByte(buffer[x]);
	}
	blWaitus(1000);

	// get and check manditory part of the reply
	if (blRxByte() | blRxByte())
	{
		return 0;
	}
	ret = blRxByte();
	if (size == 2)
	{
		ret |= ((uint16_t)blRxByte() << 8);
	}

	return ret;
}
